<template>
<!--  -->
<!-- 此页面为自定义组件：上传文件 -->
<!--  -->
  <div class="uploader-panel">
    <div class="uploader title">
        <span>上传任务</span>
        <span class="tips"> (仅展示本次上传任务) </span>
    </div>
    <div class="file-list">
        <div v-for="(item, index) in fileList" class="file-item">
            <div class="upload-panel">
                <!-- 文件名 -->
                <div class="file-name"> {{ item.fileName }} </div>

                <!-- 上传进度（百分条显示） -->
                <div class="progress">
                    <!-- 判断文件状态是否为：上传中、秒传、上传完成 -->
                    <el-progress
                      :percentage="item.uploadProgress"
                      v-if="
                        item.status == STATUS.uploading.value ||
                        item.status == STATUS.upload_seconds.value ||
                        item.status == STATUS.upload_finish
                      "></el-progress>
                </div>

                <div class="upload-status">
                    <!-- 根据文件状态选择图标 -->
                    <span 
                      :class="['iconfont', 'icon-' + STATUS[item.status].icon]"
                      :style="{ color: STATUS[item.status].color }">
                    </span>

                    <!-- 状态描述 -->
                    <span class="stauts" :style="{ color: STATUS[item.status].color }">
                        {{ item.status == "fail" ? item.errorMsg : STATUS[item.status].desc }}
                    </span>

                    <!-- 上传中的内存进度（已上传内存/文件总内存） -->
                    <span class="upload-info" v-if="item.status == STATUS.uploading.value">
                        {{ proxy.Utils.size2Str(item.uploadSize) }}/{{proxy.Utils.size2Str(item.totalSize)}}
                    </span>
                </div>
            </div>

            <div class="op">
                <!-- MD5进度 -->
                <el-progress 
                  type="circle" :width="50" 
                  :percentage="item.md5Progress" 
                  v-if="item.status==STATUS.init.value">
                </el-progress>
                 
                 <div class="op-btn">                   
                    <span v-if="item.status == STATUS.uploading.value">
                        <!-- 上传图标显示 -->
                        <Icon 
                          :width="28" 
                          class="btn-item" 
                          iconName="upload"
                          v-if="item.pause"
                          title="上传"
                          @click="startUpload(item.uid)">
                        </Icon>

                        <!-- 暂停图标 -->
                        <Icon 
                          v-else
                          :width="28" 
                          class="btn-item" 
                          iconName="pause"
                          title="暂停"
                          @click="pauseUpload(item.uid)">
                        </Icon>
                    </span>

                    <!-- X 删除图标 文件状态不能是解析中、上传完成、秒传 -->
                    <Icon 
                        v-if="
                          item.status != STATUS.init.value &&
                          item.status != STATUS.upload_finish.value &&
                          item.status != STATUS.upload_seconds
                        "
                        :width="28" 
                        class="del btn-item" 
                        iconName="del"
                        title="删除"
                        @click="delUpload(item.uid, index)">
                    </Icon>

                    <!-- 清除图标 -->
                    <Icon 
                        v-if="
                          item.status == STATUS.upload_finish.value ||
                          item.status == STATUS.upload_seconds
                        "
                        :width="28" 
                        class="clean btn-item" 
                        iconName="clean"
                        title="清除"
                        @click="cleanUpload(item.uid, index)">
                    </Icon>
                 </div>
            </div>
        </div>

        <div v-if="fileList.length == 0">
            <NoData msg="暂无上传任务"></NoData>
        </div>
    </div>
  </div>
</template>

<script setup>
import { ref, reactive, getCurrentInstance, nextTick, onMounted, watch } from "vue";
import SparkMD5 from "spark-md5"            //用于计算md5值，依赖已下载，详见package.json文件
const { proxy } = getCurrentInstance();

const api = {
    upload: "/file/uploadFile",
};

//文件上传状态
const STATUS = {
    emptyfile: {
        value: "emptyfile",
        desc: "文件为空",
        color: "#F75000",
        icon: "close",
    },
    fail: {
        value: "fail",
        desc: "上传失败",
        color: "#F75000",
        icon: "close",
    },
    init: {
        value: "init",
        desc: "解析中",
        color: "#e6a23c",
        icon: "clock",
    },
    uploading: {
        value: "uploading",
        desc: "上传中",
        color: "#409eff",
        icon: "upload",
    },
    upload_finish: {
        value: "upload_finish",
        desc: "上传完成",
        color: "#67c23a",
        icon: "ok",
    },
    upload_seconds: {
        value: "upload_seconds",
        desc: "秒传",
        color: "#67c23a",
        icon: "ok",
    },
};

const fileList = ref([]);          //本次要上传的文件列表
const delList = ref([]);           //要删除的文件列表
const chunkSize = 1024 * 1024;     //每次分片的大小，1兆(MB)

//
//文件上传 由父组件Framework.vue 的addFile方法调用
const addFile = async (file, filePid) => {
    const fileItem = {
        file: file,                 //文件：文件名、文件大小、文件流...
        uid: file.uid,              //文件UID ( 由element-ui生成 )
        md5Progress: 0,             //md5进度（md5完成后，再执行上传操作）
        md5: null,                  //md5值
        fileName: file.name,        //文件名
        status: STATUS.init.value,  //上传状态
        uploadSize: 0,              //已上传大小
        totalSize: file.size,       //文件总大小
        uploadProgress: 0,          //上传进度
        pause: false,               //暂停
        chunkIndex: 0,              //当前已上传到第几个分片
        filePid: filePid,           //文件父级ID
        errorMsg: null,             //错误信息
    };
    //向数组的开头添加一个或多个元素，并返回新的长度
    fileList.value.unshift(fileItem); 
    //如果要上传的文件大小等于零 
    if (fileItem.totalSize == 0) {
        fileItem.status = STATUS.emptyfile.value;    //将状态设置为文件为空
        return;
    }

    let md5FileUid = await computeMd5(fileItem);    //异步调用自定义的方法，计算Md5值，返回的是fileItem.uid
    console.log("md5FileUid: " + md5FileUid);
    if (md5FileUid == null) {
        return;
    }

    uploadFile(md5FileUid);                         //调用自定义方法，进行文件上传
};

defineExpose({ addFile });   //是Vue3中的一个新特性,它允许组件将其内部的数据和方法暴露给父组件

//
//计算文件的MD5
const computeMd5 = (fileItem) => {
    let file = fileItem.file;      //获得文件
    //判断浏览器（不同的浏览器有不同的处理方式）
    let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
    let chunks = Math.ceil(file.size / chunkSize);   //总片数（文件大小 / 单个分片大小，并向上取整）
    let currentChunk = 0;                            //记录第几次分片
    let spark = new SparkMD5.ArrayBuffer();          //用于计算Md5值
    let fileReader = new FileReader();               //用于将file对象转成base64

    //定义读取分片方法
    let loadNext = () => {
        let start = currentChunk * chunkSize;      //起始
        let end = start + chunkSize >= file.size ? file.size : start + chunkSize; //末尾
        fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));           //读取文件
    };
    loadNext();   //调用

    //  因为在addFile方法中是异步调用本方法，所以需要new Promise
    //参数：如果异步操作执行成功则调用resolve，反则调用reject
    return new Promise((resolve, reject) => {
        let resultFile = getFileByUid(file.uid);    //调用自定义方法，获取要进行md5计算的文件

        //   onload 是 FileReader 对象的一个事件处理函数，当读取文件成功后会被触发（上9行），
        //并接收一个e参数，通过该参数可以访问到读取的内容
        fileReader.onload = (e) => {
            spark.append(e.target.result);          //将读取到的分片追加入spark
            currentChunk++;                         //因为上面已经调用了一次loadNext()，记录读取分片次数+1

            //判断当前 记录的分片次数 < 总分片数
            if (currentChunk < chunks) {
                console.log(`文件:${file.name}, 总分片为${chunks}, Md5开始计算第${currentChunk}个分片`);
                let percent = Math.floor((currentChunk / chunks) * 100);    //计算进度(floor向下取整)
                resultFile.md5Progress = percent;                           //更新进度
                loadNext();                                                 //读取下一个分片
            } else {    //已完成所有分片的读取
                let md5 = spark.end();                         //使用字节数组作为结束符计算Md5值，并返回
                spark.destroy();                               //关闭资源 
                resultFile.md5Progress = 100;                  //Md5计算已完成，进度更新为100%
                resultFile.status = STATUS.uploading.value;    //文件更新状态为上传中
                resultFile.md5 = md5;                          //赋值 md5值
                resolve(fileItem.uid);                         //是Promise对象的方法，将Promise从等待状态转换为已完成状态，并返回值
            };
        };

        //onerror 是 FileReader 对象的一个事件处理函数，当读取文件失败时会被触发
        fileReader.onerror = () => {
            resultFile.md5Progress = -1;
            resultFile.status = STATUS.fail.value;     //更新状态为上传失败
            resolve(fileItem.uid);                     //返回uid
        };

    }).catch((error) => {
        return null;
    });
};

//
//根据uid获取文件 - uid由element-ui生成
const getFileByUid = (uid) => {
    let file = fileList.value.find(item => { //find方法，查找数组中符合条件的第一个元素，无则返回undefined
        return item.file.uid === uid;
    });
    return file;
}

const emit = defineEmits(["uploadCallback"]);  //子组件通过触发父组件的事件，并将参数传递到父组件
//
//分片上传文件 ( uid, 第几个分片开始 )
const uploadFile = async (uid, chunkIndex) => {
    chunkIndex = chunkIndex ? chunkIndex : 0;   //如未传递，则从第0片开始上传

    let currentFile = getFileByUid(uid);        //调用自定义方法，获取要进行上传的文件
    const file = currentFile.file;              //实际文件
    const fileSize = currentFile.totalSize;     //文件大小
    const chunks = Math.ceil(fileSize / chunkSize); //总片数（文件大小 / 单个分片大小，并向上取整）

    for (let i = chunkIndex; i < chunks; i++) {
        //查找是否有要删除的文件
        let delIndex = delList.value.indexOf(uid);  
        if (delIndex != -1) {
            delList.value.splice(delIndex, 1);      //从索引delIndex开始，删掉一条数据，并返回被删除的数据
            break;
        }
        //文件状态是否被暂停下载
        currentFile = getFileByUid(uid);            //文件状态可能会更新（暂停、删除等），重新获取上传文件
        if (currentFile.pause) {                    //文件状态是否被暂停下载
            break;
        }

        let start = i * chunkSize;                  //起始
        let end = (start + chunkSize) >= fileSize ? fileSize : (start + chunkSize); //末尾
        let chunkFile = file.slice(start, end);     //文件切割 (文件本身是个字节数组)
        //发送请求  
        let updateResult = await proxy.Request({
            url: api.upload,      //文件上传
            showLoading: false,   //关闭等待响应时的样式
            dataType: "file",     //参数形式
            params: {
                file: chunkFile,
                fileName: file.name,
                fileMd5: currentFile.md5,
                chunkIndex: i,
                chunks: chunks,
                fileId: currentFile.fileId,   //在上传第一个分片时，由后台生成
                filePid: currentFile.filePid,
            },
            showError: false,
            //请求出现异常时执行（详见utils/Request.js -- 43行）
            errorCallback: (errorMsg) => {   
                currentFile.status.STATUS.fail.value; //文件状态设置为上传失败
                currentFile.errorMsg = errorMsg;
            },
            //更新进度方法（详见utils/Request.js -- 103行）
            uploadProgressCallback: (event) => {
                let loaded = event.loaded;
                if (loaded > fileSize) {
                    loaded = fileSize;
                }
                currentFile.uploadSize = i * chunkSize + loaded;   //更新已下载大小
                currentFile.uploadProgress = Math.floor((currentFile.uploadSize / fileSize) * 100); //更新下载进度
            },
        });
        
        //处理请求结果
        if (updateResult == null) { break; };
        currentFile.fileId = updateResult.data.resultDto.fileId;                //上传第一个分片后生成id
        currentFile.status = STATUS[updateResult.data.resultDto.status].value;  //文件状态
        console.log("上传第一个分片后生成的id：" + updateResult.data.resultDto.fileId + " --- " + updateResult.data.resultDto.status);
        currentFile.chunkIndex = i;                                   //上传到第几个分片
        //判断状态是否为 秒传 || 上传完成
        if (updateResult.data.resultDto.status == STATUS.upload_seconds.value || 
            updateResult.data.resultDto.status == STATUS.upload_finish.value) {
                currentFile.uploadProgress = 100;      //进度100%
                emit("uploadCallback");                //刷新列表
                break;
            }
    }
}
</script>

<style lang="scss" scoped>
.uploader-panel {
    
    .uploader-title {
        border-bottom: 1px solid #ddd;
        line-height: 40px;
        padding: 0px 10px;
        font-size: 15px;
        .tips {
            font-size: 13px;
            color: rgb(169, 169, 169);
        }
    }

    .file-list {
        overflow: auto;
        padding: 10px 0px;
        min-height: calc(100vh / 2);
        max-height: calc(100vh - 120px);
        .file-item {
            position: relative;
            display: flex;
            justify-content: center;
            align-items: center;
            padding: 3px 10px;
            background-color: #fff;
            border-bottom: 1px solid #ddd;
        }
        .file-item:nth-child(even) {
            background-color: #fcf8f4;
        }
        .upload-panel {
            flex: 1;
            .file-name {
                color: rgb(64, 62, 62);
            }
            .upload-status {
                display: flex;
                align-items: center;
                margin-top: 5px;
                .iconfont {
                    margin-right: 3px;
                }
                .status {
                    color: red;
                    font-size: 13px;
                }
                .upload-info {
                    margin-left: 5px;
                    font-size: 12px;
                    color: rgb(112, 111, 111);
                }
            }
            .progress {
                height: 10px;
            }
        }
        .op {
            width: 100px;
            display: flex;
            align-items: center;
            justify-content: flex-end;
            .op-btn {
                .btn-item {
                    cursor: pointer;
                }
                .del,
                .clean {
                    margin-left: 5px;
                }
            }
        }
    }
}

</style>